home *** CD-ROM | disk | FTP | other *** search
- INTRODUCCION AL ASM: USO DE LOS PROGRAMAS ENSAMBLADORES
- =======================================================
-
- Como anticipamos en el anterior capítulo, comenzaremos aquí con el uso de
- los ensambladores comerciales. Después de algo de teoría, veremos el famoso
- 'Hello, world' tal y como quedaría en lenguaje ensamblador.
-
- Para entender bien todo el proceso desde el fichero fuente, que suele llevar
- la extensión '.ASM', hasta el programa que reside en memoria una vez que el DOS
- lo ha cargado, haremos el camino de atrás a delante, es decir, comenzando en el
- programa final y acabando en el '.ASM'.
-
- Un programa en memoria, una vez cargado, suele consistir en un bloque de
- bytes (desde unas decenas de bytes hasta algunos cientos de kilobytes), en el
- que residen tanto el código como los datos y la pila del programa. Ya que la
- longitud total puede ser superior a 64 KB, no se puede usar un solo valor de
- segmento para acceder a todo el código y a todo los datos del programa. Para
- posibilitar el acceso a todo el bloque de programa, este bloque está dividido
- en varios bloques distintos, normalmente alineados para comenzar en direcciones
- múltiplos de 16d. A estos bloques lógicos (ya que la división no es física), se
- les denomina 'segmentos'; no hay que confundir este término con los 'segmentos'
- que vimos al ver la arquitectura segmentada de los 80x86: aquellos 'segmentos'
- eran siempre de 64K bytes, mientras que estos 'segmentos' pueden tener cual-
- quier longitud menor o igual a 64K bytes.
-
- Estos bloques lógicos o segmentos se clasifican en función de lo que contie-
- nen. Principalmente, pueden contener código, datos, o constituir espacio para
- la pila. Por tanto, se habla de 'segmentos de datos', 'segmentos de código', y
- 'segmento de pila' (no suele haber más que un segmento de pila).
-
- Durante la ejecución del programa, CS suele contener un valor de forma que
- CS:0 sea el primer byte de uno de los segmentos de código, mientras que DS
- suele contener un valor de manera que DS:0 sea el primer byte de uno de los
- segmentos de datos. SS se suele mantener inalterado a lo largo del programa,
- apuntando al segmento de pila.
-
- Para saltar de un punto del programa a otro que resida en otro segmento, es
- necesario modificar tanto CS como IP, mientras que si el destino reside en el
- mismo segmento que la instrucción de salto basta con modificar IP. De la misma
- forma, para acceder a una variable que resida en el segmento de datos apuntado
- por DS no es necesario modificar DS, mientras que si la variable reside en otro
- segmento de datos es necesario cargar DS o ES para que apunte al principio del
- otro segmento antes de acceder a la variable. Como podemos intuir, el acceso
- a código y datos 'cercanos' (que residan en el CS y DS actuales) es mucho más
- cómodo, además de ligeramente más rápido, que el acceso a código y datos leja-
- nos (en otros segmentos que los apuntados por CS y DS).
-
- Más adelante volveremos a este galimatías de segmentos y veremos lo que son
- los famosos 'modelos' estándar de memoria. Por ahora, dejemos este tema y siga-
- mos.
-
- Esta imagen de memoria la crea el DOS a partir del fichero '.EXE' que el
- usuario ha ordenado ejecutar (más adelante veremos las peculiaridades de los
- '.COM'). Lo que el DOS hace no es símplemente cargar el '.EXE' tal y como está
- en el disco y saltar a la primera dirección del '.EXE', sino que el proceso de
- carga es bastante complejo. En realidad, el '.EXE' es el bloque que hemos
- visto, compuesto de varios bloques lógicos (llamados segmentos), y precedido de
- una cabecera de longitud variable. El estudio detallado de esta cabecera queda
- fuera de nuestro ámbito, pero nos interesa saber el contenido aproximado.
-
- Los primeros dos bytes del '.EXE' son la cadena 'MZ', que lo identifican
- como '.EXE'. La cabecera indica, además, su propia longitud, un CRC para el
- fichero completo (es decir, una suma utilizada para verificar la integridad del
- fichero), el punto - dentro del bloque de código y datos - al que hay que sal-
- tar (el punto de entrada al programa), los valores iniciales de SS y SP para
- apuntar a la pila, y algún detalle más. Pero, además de todo esto, lo más
- importante de la cabecera es la llamada 'tabla de reubicación'.
-
- En un sistema MSDOS, los programas deben ser capaces de correr en cualquier
- punto de la memoria. Las instrucciones de los procesadores 80x86 son en muchos
- casos relativas (es decir, un salto suele ser 'salta 10 bytes más adelante' y
- no 'salta a la dirección 310'), con lo que gran parte del código funciona en
- cualquier punto de la memoria. Pero existen algunas instrucciones que utilizan
- direcciones absolutas, como por ejemplo cargar en un registro la dirección
- completa de una variable (offset y segmento). Estas instrucciones tienen que
- ser distintas cuando los datos están en la zona baja de la memoria o en la zona
- alta. Para solucionar este problema, un '.EXE' lleva en la cabecera una tabla
- en la que se indican todas las instrucciones que necesitan ser modificadas al
- cargar el programa.
-
- Por tanto, el proceso de carga es el siguiente (realizado por el MSDOS):
-
- - Carga del '.EXE' en dos partes: cabecera y bloque de programa.
- - Recorrido de la tabla de reubicación, arreglando cada referencia
- marcada en la tabla.
- - Inicialización de los registros de segmento, tal y como viene
- especificado en la cabecera, y salto al punto de entrada.
-
- Hay que especificar dos cosas en es punto, antes de pasar a los ficheros
- '.OBJ': la primera, que todas las referencias de la tabla de reubicación no son
- para instrucciones, sino que algunas pueden ser para datos. Esto ocurre en el
- caso de que una variable estática, al ser declarada, sea inicializada con un
- valor que dependa de la situación el memoria de otra variable. La segunda pun-
- tualización es que el MSDOS se encarga de inicializar algunos registros de seg-
- mento antes de entrar al programa, pero no todos. CS, evidentemente, apunta al
- segmento de código que contien el punto de entrada. SS apunta ya al segmento de
- pila. Pero DS, en principio, no apunta a ningún sitio en concreto. Por ello,
- las primeras instrucciones de un programa ASM suelen ser para inicializar DS
- apuntando al segmento de datos que vayamos a usar.
-
- Un fichero '.EXE' como los que hemos visto lo construye un programa que se
- incluye con cualquier compilador o ensamblador, llamado enlazador o LINKer (en
- spanglish). Los linkers de Microsoft suelen llamarse LINK.EXE, mientras que los
- de Borland se llaman siempre TLINK.EXE.
-
- Un linker toma como entrada un conjunto de uno o varios ficheros llamados
- 'ficheros objeto', y genera como salida un fichero '.EXE' (opcionalmente, el
- TLINK puede generar un '.COM', mientras que con el LINK es necesario convertir
- el '.EXE' resultante en '.COM' con la utilidad EXE2BIN). Los ficheros objeto
- tienen extensión '.OBJ', y son ficheros generados directamente por los ensam-
- bladores y compiladores a partir de nuestros listados con el código fuente.
-
- Un fichero '.OBJ' contiene varios bloques, que son los que se usan luego
- para construir los segmentos que van en el '.EXE'. Además de estos bloques, los
- ficheros '.OBJ' llevan también algunas referencias que luego deberán ser in-
- cluidas en la tabla de reubicación del '.EXE'. Pero lo que diferencia princi-
- palmente a un '.OBJ' de un '.EXE' (aparte del formato de fichero), son algo
- llamado 'referencias externas' y 'declaraciones públicas'.
-
- Para facilitar el trabajo del programador, los lenguajes actuales permiten
- que un programa '.EXE' se construya a partir de código de varios listados
- diferentes. Además, cada uno de estos 'módulos' (que así se llaman) puede estar
- escrito en un lenguaje diferente, siempre que tengamos un compilador para ese
- lenguaje que genere ficheros '.OBJ' estándar. Una de las complicaciones que
- esta posibilidad añade, es que es necesario poder acceder desde un módulo a
- funciones y variables declaradas en otro módulo. Lo que se hace es permitir a
- cualquier módulo hacer declaraciones externas, en las que se indica al compila-
- dor que una variable o función está en algún otro módulo con el que luego
- linkaremos éste. Así, el '.OBJ' generado incluye precisamente una referencia
- externa con el nombre de la función o variable. El módulo que define una varia-
- ble o función a la que se quiere acceder desde otro módulo debe declarar a ésta
- como 'pública', de manera que el '.OBJ' generado lleve una pequeña indicación
- advirtiendo que la variable de ese nombre está en ese módulo, junto con la
- dirección de ésta. El linker es el que se encarga de resolver todas estas refe-
- rencias, modificar todas las instrucciones que sea posible en este punto y
- dejar las que no se puedan arreglar en la tabla de reubicación del '.EXE'.
-
- Otra de las cosas que hace el linker es 'unir' los segmentos de distintos
- módulos en uno sólo. Ya que cada segmento del '.OBJ' va acompañado de un nombre
- y de algunos datos, si estos coinciden el linker supone que se ha hecho así
- intencionadamente y los une en uno sólo, poniendo uno a continuación del otro.
- Las reglas que sigue el linker para reconocer los segmentos y actuar en conse-
- cuencia son, junto con el eslabón perdido, uno de los misterios más ignotos de
- la humanidad. Ahora en serio, las reglas que siguen en LINK y el TLINK para
- reconocer el segmento de pila, etc... son ligeramente distintas, pero existe
- una manera de hacer todo esto sin preocuparse de los nombres de los segmentos,
- y, por ejemplo, linkar un módulo ASM con uno escrito en C y conseguir fundir
- los segmentos de código y datos de los dos módulos. Para esto usaremos carácte-
- rísticas del MASM 5.0 y superiores, y del TASM 1.0 y superiores, por lo que os
- recomiendo que consigáis al menos estas versiones de los ensambladores (con uno
- de los dos vale).
-
- Además de los ficheros '.OBJ', un linker puede tambier linkar ficheros
- '.LIB'. Un '.LIB' o fichero de librería es un fichero que contiene un conjunto
- de ficheros '.OBJ', uno detrás de otro, que por alguna razón se suelen usar
- conjuntamente. Además, el '.LIB' lleva un índice hasheado para encontrar rápi-
- damente las referencias a símbolos del '.LIB'. Cuando el linker encuentra una
- referencia a un símbolo externo, primero lo busca en los '.OBJ' especificados
- por el usuario, y en caso de no encontrarlo busca en los índices de las '.LIB'
- especificadas. Si lo encuentra, extrae el '.OBJ' de la '.LIB' en el que apare-
- ce el símbolo (variable o función) y lo añade al '.EXE' generado. Por ejemplo,
- todas las funciones que un compilador de C provee al programador vienen en una
- librería (llamada librería estándar). Es bastante sencillo extraer de un '.LIB'
- los '.OBJ' que lo componen, aunque la mayoría de los gestores de librería no lo
- permiten.
-
- Ahora que ya sabemos cómo son los '.OBJ', el estudio del listado fuente de
- programa en ASM resulta bastante sencillo: un listado fuente en lenguaje ensam-
- blador es prácticamente una imagen del '.OBJ' que se quiere generar. El
- listado está dividido en fragmentos, denominados 'segmentos', que son los blo-
- ques que el '.OBJ' lleva para el linker. Estos segmentos pueden ser de código
- o de datos. Además, podemos encontrar declaraciones PUBLIC (símbolos que se
- exportan en el '.OBJ' para uso y disfrute generalizado de los demás módulos);
- podemos también encontrar declaraciones EXTRN (símbolos que se indican en el
- '.OBJ' al linker para ser buscados entre los demás módulos).
-
- Veamos ya cómo quedaría en ASM el famoso 'Hello, world!':
-
- =================8<============================8<===========================
- PILA SEGMENT STACK 'STACK' ; Abre el segmento de pila
- DW 100h DUP (?) ; 200h (512d) bytes de pila
- PILA ENDS ; Cierra el segmento de pila
-
- DATOS SEGMENT 'DATA' ; Abre el segmento de datos
- Msg DB 'Hello, world!$' ; Mensaje a imprimir
- DATOS ENDS ; Cierra el segmento de datos
-
- CODIGO SEGMENT 'CODE' ; Abre el segmento de código
- ASSUME CS:CODIGO, DS:DATOS, SS:PILA
- Entrada PROC ; Abre el procedimiento 'Entrada'
- mov ax,DATOS ; Valor de segmento para 'DATOS'
- mov ds,ax ; Para acceder a 'Msg'
- mov dx,OFFSET Msg ; Para la int 21h, servicio 9
- mov ah,9 ; Especifica servicio9
- int 21h ; Invoca servicio 9: imprimir cadena
- mov ax,4C00h ; Servicio 4Ch, valor de retorno 0
- int 21h ; Invoca servicio 4Ch: retorno al DOS
- Entrada ENDP ; Cierra el procedimiento 'Entrada'
- CODIGO ENDS ; Cierra el segmento 'CODIGO'
-
- END Entrada ; Fin del programa, punto de entrada 'Entrada'
- =================8<============================8<===========================
-
- Los ensambladores más modernos permiten una manera más sencilla de escribir
- este programa, usando las directivas de segmento simplificadas, pero creo que
- es mejor ver primero la forma 'clásica' y luego la simplificada, con lo que la
- comprensión es mucho mayor.
-
- Como vemos, el listado está dividido en tres partes principales, que corres-
- ponden directamente a los segmentos del '.OBJ'. El principio de un segmento se
- indica con la directiva del ensamblador 'SEGMENT', y el final con la directiva
- 'ENDS'. Hay que tener en cuenta que estas directivas no son instrucciones en
- ASM, sino que indican al ensamblador cómo debe realizar su trabajo, por lo que
- no generan código. Al final del listado siempre debe aparecer la directiva END,
- que indica el final del listado. Opcionalmente, puede llevar un argumento cuyo
- significado veremos más tarde.
-
- El trabajo del ensamblador es básicamente el siguiente: va leyendo líneas
- del fichero '.ASM', que serán siempre directivas. Cuando encuentra una línea
- con la directiva SEGMENT, prepara un buffer para ir introduciendo en él los
- datos que irá leyendo. Inicializa un contador interno a 0, para apuntar al
- principio de buffer. Entonces, comienza a procesar las líneas de otra forma
- hasta que encuentra un ENDS, que indica el final del segmento. El contenido del
- buffer irá al '.OBJ' prácticamente tal y como está. Las líneas interiores a un
- segmento (entre un SEGMENT y su ENDS correspondiente) se procesan de otra
- forma. Aunque algunas pueden ser directivas, la mayoría están destinadas a ge-
- nerar código o datos que irán en el '.OBJ'. Después de almacenar los bytes que
- una línea define en el buffer, el contador interno se incrementa para apuntar
- a los siguientes bytes libres. Estas líneas pueden ser líneas de código o líne-
- as de datos. Si son de código, la sintáxis será la siguiente:
-
- ETIQUETA: INSTRUCCION OPERANDO(S) ;COMENTARIO
-
- Mientras que si son líneas de datos, la sintáxis será la siguiente:
-
- ETIQUETA D<X> DATOS ;COMENTARIO
-
- En esta línea, <X> se sustituye por una letra que indica el tamaño de los
- datos (B: bytes, W: palabras, D:dobles palabras, ...).
-
- La etiqueta es un símbolo, en principio único (no puede haber dos etiquetas
- iguales en un mismo listado), que el ensamblador almacena en una tabla de sím-
- bolos interna junto con el valor del contador interno al procesar la línea.
- También se almacena el contexto en el que aparece la etiqueta (si es de datos
- o de código, el tamaño de los datos a los que apunta, el segmento al que perte-
- nece...). Se utiliza para referenciar desde otro punto una variable por nombre,
- en caso de que se trate de una etiqueta de datos, o para saltar a un punto
- determinado desde otro lugar del programa.
-
- La instrucción y los operandos son cualquier instrucción válida del 80x86, y
- el comentario puede ser cualquier cosa que aclare la instrucción comentada. Los
- datos de una línea del segundo tipo pueden ser números separados por comas,
- en cuyo caso se sitúan uno detrás del siguiente. Cuando no se quiere iniciali-
- zar una variable, se puede poner un símbolo de interrogación ('?'). Si se quie-
- re repetir varias veces una secuencia, se puede utilizar la construcción:
-
- n DUP (dato, dato, dato)
-
- Donde 'n' es un número que representa cuántas veces se debe repetir la se-
- cuencia.
-
- Estudiemos ahora con detalle el listado HELLO.ASM. Para hacer la prueba, se
- puede recortar el listado y salvarlo como HELLO.ASM. Después, podemos ensamblar
- con 'TASM HELLO' o con 'MASM HELLO;'. El linkado lo haremos con 'TLINK HELLO' o
- 'LINK HELLO;', tras lo cual tendremos el HELLO.EXE en el directorio actual. La
- sintáxis de la línea de comandos varía ligeramente de una versión a otra del
- mismo compilador, por lo que os recomiendo consultar el manual o hacer algunas
- pruebas.
-
- Las primeras tres líneas del listado proporcionan una segmento para la pila
- del programa. Como vemos, el segmento lleva un nombre que le proporcionamos
- nosotros, en este caso el nombre 'PILA'. Este nombre es cualquier identificador
- válido en ASM; recordemos que el ensamblador no distingue entre mayúsculas y
- minúsculas. Después de la directiva SEGMENT van varios parámetros opcionales.
- Ya que normalmente usaremos las directivas de segmento simplificadas, no entra-
- remos a detallar estos parámetros. Si alguien tiene mucho interés puede consul-
- tar el manual del ensamblador o la ayuda online de éste, en caso de que tenga.
- Como se aprecia, se dejan 100h (256d) palabras sin inicializar para la pila.
- Esto suele ser suficiente para programas que no usen la pila intensivamente.
-
- Las siguientes tres líneas definen otro segmento, que lleva los datos del
- programa. En este caso, la única variable es una cadena con el mensaje a impri-
- mir. Cuando tras un DB aparece una cadena entre comillas simples, el ensambla-
- dor introduce todos los caracteres, uno detras de otro.
-
- Para no hacer el mensaje demasiado largo, continuaremos el estudio de éste
- listado en el siguiente capítulo.
-
- Salut :-)
-
- Jon